<?php

1->事物处理
   问题: 我们在方法上使用'@Transactional'标注, 但是执行流程中有'try-catch'包裹事务操作, 那么这个时候异常'不会事务回滚'!
   原因: 因为事务的生效是要'抛出异常'才会执行'rollback', 但是我们使用了'try-catch', 导致没有对外抛出异常, 所以不会回滚!
   解决:
       1). 不要使用'try-catch'包裹事务处理代码.
       2). 不得已使用'try-catch'的话, 可以手动在'catch'中指定当前事务回滚:
           try {
             ...
           } catch(Exception e) {
              // 1. 设置当前事务手动执行回滚.
              TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
              // 2. 手动抛异常
              throw e;
           }


2->事物方法调用 (@Transactional)
   异常场景:
   场景1: 都没有事务 (事务都不回滚)
   场景2: 主方法有事务, 子方法没有事务 ('事务传递性' 都回滚)
   场景3: 主方法没有事务, 子方法有事务 (都不回滚, 配置可实现子方法回滚)

   原因:
   因为Spring的bean是'单例模式', 所以框架初始化完毕之后就确定'是否使用事务', 事务调用时有'传递性', 如果一开始的方法有事务,
   那么就会将事务'传递下游方法'; 反观, 如果一开始没有事务的话, 那么将'不会事务传递'!

   解决: 也可以通过设置满足'场景三'
   方案1 (推荐):
   @EnableAspectJAutoProxy(exposeProxy = true)
   UserService userService = (UserService) AopContext.currentProxy();
   userService.saveOne4Children(params);

   方案2 (不推荐):
   将子方法'抽取新类中', 然后在主方法中调用, 虽然该方案实现简单, 但是有可能会产生大量'冗余代码'

   注意: 异常如果发生在'主方法没有事务'的话, 那么即使子方法这样设置了, 也是不会回滚的, 要想子方法回滚, 那么就要在该子方法内部抛出异常!

   总结:
   . 主方法'没有'事务的话, 那么子方法即使有事务注解也相当于没有 (可配置);
   . 主方法'有'事务的话: 一旦主方法抛出异常, 那么不管子方法是否有事务, 都会回滚, 因为当前操作都在[一个事务管理中]



3->Mysql和Redis的双写一致性
   https://zhuanlan.zhihu.com/p/59167071
   https://www.cnblogs.com/rjzheng/p/9041659.html

   问题: mysql和redis的数据同步中, 对于redis来说, 是'更新缓存'还是'删除缓存'
   结论: 一了百了, 删除缓存
   原因:
   1. 多个线程做'缓存更新操作', 线程A更新缓存'应该'比请求B更新缓存早才对, 但是因为网络等原因, B却比A更早更新了缓存, 这就导致了'脏数据', 因此不考虑
   2. 如果'写多场景'的话, 数据还没读到就被'频繁更新', 浪费性能!
      而且如果写入数据库的数据'并不是'可以直接放入缓存的, 而是需要经过'一系列复杂操作'才能放入缓存, 那么就太耗费性能了!
      所以, 直接'删除缓存', 一了百了!

   A. 先'删除'缓存，再更新数据库
     - [问题]
        线程A更新数据, 这时线程B来查询数据, 此时发现没有缓存数据, 那么线程B就去查库, 然后写入缓存,
        这时线程A更新完数据库, 将数据也写入缓存, 那么此时缓存中的数据其实就是'脏数据' (因为'读快写慢')

     - <方案>
      1. 设置过期时间 (虽然这种方式可能会造成脏数据, 但是在缓存过期时就会同步数据)
      2. 延时双删 (在首次删除后, sleep一段时间(s / ms) 后, 再次删除, 保证后续请求读到新的数据)
         如果要保证'高吞吐'量的话, 那么延时删除的可以用'异步 - [异步线程 / MQ]'都可以
         public void write(String key,Object data) {
          		redis.delKey(key);
        	    db.updateData(data);
        	    Thread.sleep(1000);
        	    redis.delKey(key);     // 再次删除, 防止 [读快写慢]
         }

   B. 先更新数据库，再'删除'缓存
     - [问题]
       线程A更新数据, 这时线程B来查询数据, 此时缓存刚好过期, 那么线程B就去查库得到旧值
       然后线程A将新值写入数据库, 然后删除缓存, 这时线程B反应慢, 将旧值写入缓存
       那么, 这样一来, 就又发生'脏数据'了

     - <方案>
       1. 设置过期时间
       2. 延时双删

   [[ 延时双删.衍生问题 ]]
   那么如果在延时双删策略下, '延时删除'的删除失败了, 应该怎么办...
   拿捏: 提供一个保障的重试机制即可
   1. 在延时删除失败时, 将失败的删除通过'MQ'再次删除, 直到成功
      缺点: 该方案对业务线代码造成'大量的侵入', 优美的解决...

   2. blog模式
      . 启动一个订阅程序去订阅数据库的binlog, 获得需要操作的数据, 在应用程序中, 另起一段程序, 获得这个订阅程序传来的信息, 进行删除缓存操作.
      . 然后如果失败的话, 就交给'MQ'直到成功
        (阿里系开源中间件 - Canal, 就可以完美适配)

   总结:
   其实我们发现, 并没有'绝对的'解决方案, 我们所做的只不过是: 尽可能保证[mysql]和[redis]的数据一致性, 这种一致性并不是'瞬时一致性', 而是'最终一致性',
   双方都尽最大可能使得彼此保持'数据一致'



4->Redis的锁超时时怎么处理
   问题描述: 我们使用Redis做并发锁控制时, 如果一个方法还没有结束 (可能调用三方被拖住了...), 导致锁的超时时间到了, 那么怎么保证呢...
   锁释放时机:
   . 锁的超时时间到了之后 -> 释放
   . 代码正常执行完毕后 -> 释放
   思路:
   我们其实并不想让我们的业务没有执行完毕就释放掉锁的, 所以能不能不释放, 在执行业务时, 额外起一个'守护线程 - 看门狗', 去进行key的续期
   续期成功: 说明任务还在执行, key也还没有过期
   续期失败: 说明任务已经执行完毕, 那么就不需要继续续期

   注意: 该守护线程需要'等待一段'时间后再执行, 避免资源浪费 (锁超时时间 / 2)

   code:
   step1: 设置守护线程
   public class RedisLockDog {

       private String lockKey;
       private String lockValue;
       private long lockTime;      // 毫秒
       public RedisLockDog(String lockKey, String lockValue, long lockTime) {
          this.lockKey = lockKey;
          this.lockValue = lockValue;
          this.lockTime = lockTime;
          this.isRenewal = Boolean.TRUE;
       }

       void stop() {
          this.isRenewal = Boolean.FALSE;
       }

       while(isRenewal) {

           int waitTime = lockTime / 2;
           Thread.sleep(waitTime);
           String luaStr = " if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return '0' end ";
           Object obj = jedis.eval(luaStr, Collections.singletonList(key), Collections.singletonList(lockTime));
           if(1.equals(obj)) {
              // 续期 -> 成功
           } else {
              // 续期 -> 失败
              this.stop();
           }
       }

   }

   step2: 使用
   String lockKey = "lockKey";
   String lockValue = "lockValue";
   long lockTime = 3000;
   if(redisLock.lock(lockKey, lockValue, lockTime)) {
       // 开始守护线程
       RedisLockDog lockDog = new RedisLockDog(lockKey, lockValue, lockTime);
       Thread t = new Thread(lockDog);
       t.setDaemon(true);
       t.start();
       // 业务逻辑 -> 开始执行

       // 业务逻辑 -> 执行完毕
       lockDog.stop();
       t.interrupt();
       
   }













.
